import numpy 
from . import DEFAULT_RTOL, DEFAULT_ATOL

class Comparison(object):
    '''

    Comparison(**kwargs)
    
    Create an object for storing a comparison expression.

    A comparison expression comprises a relation (such as 'less than')
    and a value (any object capable of being related to with the
    standard comparison operators). These are stored in the `relation`
    and `value` attributes respectively.

    The comparison is evaluated for an arbitrary object with the
    `evaluate` method.

    Valid relations are:

    ========   =============================================
    Relation   Description
    ========   =============================================
    lt         Strictly less than
    le         Less than or equal
    gt         Strictly greater than
    ge         Greater than or equal
    eq         Equals (within a tolerance)
    ne         Not equal to (within a tolerance)
    inside     Inside a given range (range bounds included)
    outside    Outside a given range (range bounds excluded)
    ========   =============================================
 
    See also
    --------
    As a convenience, for each relation in the above list there is an
    identically named function which returns the appropriate
    `cf.Comparison` object.

    Parameters
    ----------
    **kwargs: 
        A keyword argument to the call is the relation of the
        comparison (any one of the relations listed above). The
        keyword's argument is the object to test the comparison
        against. If the keyword argument is 'inside' or 'outside' then
        the value should be a two-element list or tuple.

    Attributes
    ----------
    relation : str   
        A string definging the comparison operator. One of relations
        given above.
    value : object       
        The object with which to compare.

    Methods
    -------
    dump     : Return a string containing a full description of the
               object.
    evaluate : Evaluate the comparison for a given variable.

    Examples
    --------
    >>> c = Comparison(le = 5)
    >>> c.evaluate(4)
    True
    >>> c.evaluate(5)
    True

    >>> c = lt(5)
    >>> c.evaluate(4)
    True

    >>> c = Comparison(inside = (1,2))
    >>> a = numpy.arange(4)
    >>> c.evaluate(a)
    array([False,  True,  True, False], dtype=bool)
    >>> (a >= 1) & (a <= 2)
    array([False,  True,  True, False], dtype=bool)

    '''

    def __init__(self, **kwargs):

        for key, value in kwargs.items():
            self.relation = key
            self.value    = value
    #--- End: def Comparison.__init__

    def __repr__(self):

        return '<CF '+str(self)+'>'
    #--- End: def Comparison.__repr__

    def __str__(self):

        return 'Comparison: x '+self.relation+' '+repr(self.value)
    #--- End: def Comparison.__str__

    def dump(self, id=None, nc=False, omit=()):
        
        return str(self)
    #--- End: def Comparison.dump

    def evaluate(self, x):
        '''

    Evaluate the comparison for a given object.

    Arguments
    ---------
    x : object

    Returns
    -------
        Typically a boolean or array of booleans. More generally, the
        output from the appropriate relation operators (==, !=, <, >,
        <=, >=) for the given variable, x.
        
        '''
        if self.relation == 'lt':
            return x <  self.value
        elif self.relation == 'le':
            return x <= self.value
        elif self.relation == 'gt':
            return x >  self.value
        elif self.relation == 'ge':
            return x >= self.value
        elif self.relation == 'eq':
            return x == self.value
        elif self.relation == 'ne':
            return x != self.value
        elif self.relation == 'inside':
            if self.value[0] < self.value[1]:
                return (x >= self.value[0]) & (x <= self.value[1])
            else:
                return (x >= self.value[1]) & (x <= self.value[0])    
        elif self.relation == 'outside':
            if self.value[0] < self.value[1]:
                return (x <  self.value[0]) | (x >  self.value[1])
            else:
                return (x <  self.value[1]) | (x <  self.value[0])    
    #--- End: def evaluate
#--- End: class Comparison

def lt(value):
    '''
    
    Create a Comparison object for evaluating whether a variable is
    less than the given value.

    Arguments
    ---------
    value : object
        The `value` attribute in the returned `cf.Comparison` object.

    Returns
    -------
        A `cf.Comparison` object whose `relation` attribute is set to
        'lt' and whose `value` attribute set to the `value` argument.

    '''
    return Comparison(lt=value)
    
def le(value):
    '''
    
    Create a Comparison object for evaluating whether a variable is
    less than or equal to the given value.

    Arguments
    ---------
    value : object
        The `value` attribute in the returned `cf.Comparison` object.

    Returns
    -------
        A `cf.Comparison` object whose `relation` attribute is set to
        'le' and whose `value` attribute set to the `value` argument.

    '''
    return Comparison(le=value)
    
def gt(value):
    '''
    
    Create a Comparison object for evaluating whether a variable is
    greater than the given value.

    Arguments
    ---------
    value : object
        The `value` attribute in the returned `cf.Comparison` object.

    Returns
    -------
        A `cf.Comparison` object whose `relation` attribute is set to
        'gt' and whose `value` attribute set to the `value` argument.

    '''
    return Comparison(gt=value)
    
def ge(value):
    '''
    
    Create a Comparison object for evaluating whether a variable is
    greater than or equal to the given value.

    Arguments
    ---------
    value : object
        The `value` attribute in the returned `cf.Comparison` object.

    Returns
    -------
        A `cf.Comparison` object whose `relation` attribute is set to
        'ge' and whose `value` attribute set to the `value` argument.

    '''
    return Comparison(ge=value)
    
def eq(value):
    '''
    
    Create a Comparison object for evaluating whether a variable is
    equal to the given value.

    Arguments
    ---------
    value : object
        The `value` attribute in the returned `cf.Comparison` object.

    Returns
    -------
        A `cf.Comparison` object whose `relation` attribute is set to
        'eq' and whose `value` attribute set to the `value` argument.

    '''
    return Comparison(eq=value)
    
def ne(value):
    '''
    
    Create a Comparison object for evaluating whether a variable is
    not equal to the given value.

    Arguments
    ---------
    value : object
        The `value` attribute in the returned `cf.Comparison` object.

    Returns
    -------
        A `cf.Comparison` object whose `relation` attribute is set to
        'ne' and whose `value` attribute set to the `value` argument.

    '''
    return Comparison(ne=value)
    
def inside(value0, value1):
    '''
    
    Create a Comparison object for evaluating whether a variable is
    inside the given range.

    Arguments
    ---------
    value0 : object
        The first element of `value` attribute tuple in the returned
        `cf.Comparison` object.
    value1 : object
        The second element of `value` attribute tuple in the returned
        `cf.Comparison` object.

    Returns
    -------
        A `cf.Comparison` object whose `relation` attribute is set to
        'inside' and whose `value` attribute set to the tuple
        (`value0`,`value1`).

    '''
    return Comparison(inside=(value0, value1))
    
def outside(value0, value1):
    '''
    
    Create a Comparison object for evaluating whether a variable is
    outside the given range.

    Arguments
    ---------
    value0 : object
        The first element of `value` attribute tuple in the returned
        `cf.Comparison` object.
    value1 : object
        The second element of `value` attribute tuple in the returned
        `cf.Comparison` object.

    Returns
    -------
        A `cf.Comparison` object whose `relation` attribute is set to
        'outside' and whose `value` attribute set to the tuple
        (`value0`,`value1`).

    '''
    return Comparison(outside=(value0, value1))
    
def _set_tolerances(x, y, rtol=None, atol=None, override=True):
    '''
    '''
    out = []

#    default = cf.DEFAULT_RTOL
    default = DEFAULT_RTOL
    for arg, tol in zip((  rtol ,   atol ),
                        ('_rtol', '_atol')):

        if arg is not None:
            if override:
                # Use the given argument
                pass
            else:
                if hasattr(x, tol) and getattr(x, tol) is not None:
                    arg = getattr(x, tol)                  
                elif hasattr(y, tol) and getattr(y, tol) is not None:
                    arg = getattr(y, tol)                    
                else:
                    # Use the given argument
                    pass
        else:
            # The given argument is None            
            if hasattr(x, tol) and getattr(x, tol) is not None:
                arg = getattr(x, tol)                   
            elif hasattr(y, tol) and getattr(y, tol) is not None:
                arg = getattr(y, tol)
            else:
                arg = default
                
        try:
            arg.__iter__
            arg = default
        except AttributeError:
            pass

        out.append(arg)

#        default = cf.DEFAULT_ATOL
        default = DEFAULT_ATOL

    #--- End: for arg, tol

    return tuple(out)
#--- End: def _set_tolerances

def equals(x, y, rtol=None, atol=None, override=True):
    '''Ascertain if x equals y, assuming that they're 1) space
    components, 2) numbers or 3) anything else, in that order.

    dch
    '''

    try: 
        if not x.equals(y, rtol=rtol, atol=atol, override=override):
            return False
    except (AttributeError, TypeError):        
        try:
            if not y.equals(x, rtol=rtol, atol=atol, override=override):
                return False
        except (AttributeError, TypeError):
            # Set rtol and atol
            rtol, atol = _set_tolerances(x, y, rtol=rtol, atol=atol,
                                         override=override)
            try:
                if not numpy.allclose(x, y, rtol=rtol, atol=atol):
                    return False
            except (TypeError, IndexError, NotImplementedError):
                if x != y:
                    return False  

    return True
#--- End: def equals

def dump(x, id=None, nc=False, omit=()):
    '''
    
    Print the full description of an object using its `dump` method.
    
    This function is exactly equivalent to:

    >>> print x.dump(...)

    or, if the object has no `dump` method:

    >>> print x

    Arguments
    ---------
    x : object
        The object to print.

    Parameters
    ----------
    The parameters are only used if the object's `dump` method is
    being utilized

    id : str, optional
        Set the common prefix of variable component names. If None
        then defaults to the variable's class name.
    nc : bool, optional
        If True then include attributes whose names begin 'nc'.        
    omit: sequence, optional
        Omit the given attributes from a variable's description.

    Returns
    -------
        None
        
        '''
    try:
        print x.dump(id=id, nc=nc, omit=omit)
    except AttributeError:
        print x
#--- End: def dump
        
